import functools
import hashlib
from time import time

from flask_caching import function_namespace, get_arg_names, get_arg_default
from flask_caching.backends.simplecache import SimpleCache


class MemCache(SimpleCache):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def _get_expiration(self, timeout):
        if timeout is None:
            timeout = self.default_timeout
        if timeout > 0:
            timeout = time() + timeout
        return timeout

    def get(self, key, default=None):
        try:
            expires, value = self._cache[key]
            if expires == 0 or expires > time():
                return value
            return default
        except KeyError:
            return default

    def set(self, key, value, timeout=None):
        expires = self._get_expiration(timeout)
        self._prune()
        self._cache[key] = (expires, value)
        return True

    def add(self, key, value, timeout=None):
        expires = self._get_expiration(timeout)
        self._prune()
        item = (expires, value)
        if key in self._cache:
            return False
        self._cache.setdefault(key, item)
        return True

    def keys(self):
        return self._cache.keys()


class Cache:
    def __init__(self, config):
        config.setdefault('default_timeout', 300)
        self.__set_cache(config)

    def __set_cache(self, config):
        cache_type = config.pop('cache_type')
        if cache_type == 'MemCache':
            config.setdefault('threshold', 10000)
        self._cache = globals()[cache_type](**config)

    @property
    def cache(self):
        return self._cache

    def count(self):
        return len(self._cache.keys())

    def get(self, *args, **kwargs):
        return self.cache.get(*args, **kwargs)

    def set(self, *args, **kwargs):
        return self.cache.set(*args, **kwargs)

    def add(self, *args, **kwargs):
        return self.cache.add(*args, **kwargs)

    def delete(self, *args, **kwargs):
        return self.cache.delete(*args, **kwargs)

    def delete_many(self, *args, **kwargs):
        return self.cache.delete_many(*args, **kwargs)

    def clear(self):
        return self.cache.clear()

    def get_many(self, *args, **kwargs):
        return self.cache.get_many(*args, **kwargs)

    def set_many(self, *args, **kwargs):
        return self.cache.set_many(*args, **kwargs)

    def memoize(self, timeout=None):
        def memoize(f):
            @functools.wraps(f)
            def decorated_function(*args, **kwargs):
                try:
                    cache_key = self.make_cache_key(
                        f, *args, **kwargs
                    )
                    rv = self.cache.get(cache_key)
                except Exception:
                    return f(*args, **kwargs)

                if rv is None:
                    rv = f(*args, **kwargs)
                    try:
                        self.cache.set(
                            cache_key,
                            rv,
                            timeout=timeout,
                        )
                    except Exception as e:
                        print(e)
                        pass
                return rv

            return decorated_function

        return memoize

    def make_cache_key(self, f, *args, **kwargs):
        f_name, _ = function_namespace(f, args)
        if callable(f):
            key_args, key_kwargs = self._memoize_kwargs_to_args(
                f, *args, **kwargs
            )
        else:
            key_args, key_kwargs = args, kwargs

        updated = f'{f_name}{key_args}{key_kwargs}'
        cache_key = hashlib.md5(updated.encode('utf-8')).hexdigest()
        return cache_key

    @staticmethod
    def _memoize_kwargs_to_args(f, *args, **kwargs):
        new_args = []
        arg_num = 0
        new_kwargs = kwargs.copy()
        kw_keys_remaining = list(kwargs.keys())
        arg_names = get_arg_names(f)
        args_len = len(arg_names)

        for i in range(args_len):
            arg_default = get_arg_default(f, i)
            if i == 0 and arg_names[i] in ('self', 'cls'):
                arg = repr(args[0])
                arg_num += 1
            elif arg_names[i] in kwargs:
                arg = kwargs[arg_names[i]]
                kw_keys_remaining.pop(kw_keys_remaining.index(arg_names[i]))
                new_kwargs.pop(arg_names[i])
            elif arg_num < len(args):
                arg = args[arg_num]
                arg_num += 1
            elif arg_default:
                arg = arg_default
                arg_num += 1
            else:
                arg = None
                arg_num += 1

            new_args.append(arg)

        new_args.extend(args[len(arg_names):])
        return (
            tuple(new_args), sorted(new_kwargs.items())
        )
